﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Threading.Tasks;

using Optimization.Utilities;

namespace Optimization.Experiment
{
    public class ExperimentDriver
    {

        /// <summary>
        /// Enumerates info on ExperimentCollection
        /// </summary>
        /// <param name="input">ExperimentCollection which have to be reported</param>
        /// <returns></returns>
        static public IEnumerable<string> MakeReport(ExperimentCollection input)
        {
            int[] seeds = input.GroupBy((d) => d.Seed).Select(gr => gr.Key).ToArray();
            //int[] seeds = input.Aggregate(new List<int>(), (list, ExDesc) => { list.Add(ExDesc.Steps); return list; }).ToArray();

            int[] steps = input.Aggregate(new List<int>(),
                (list, exDesc) =>
                {
                    list.AddRange(exDesc.Solutions.Where( s => !list.Contains(s.Step)).Aggregate(new List<int>(),
                        (innerList, solution) =>
                        {
                            innerList.Add(solution.Step); return innerList;
                        }));
                    return list;
                }).ToArray();

            yield return ("=== Problems ===");
            foreach (string problemName in input.GroupBy((d) => d.Problem.AsString()).Select(gr => gr.Key))
                yield return (problemName);

            yield return ("=== Solvers ===");
            //foreach (string solverName in input.GroupBy((d) => d.Solver.Name + d.Properties.Aggregate((s1, s2)=>(s1 + s2))).Select(gr => gr.Key))
            //    yield return (solverName);
            foreach (string solverName in input.GroupBy((d) => d.Solver.AsString()).Select(gr => gr.Key))
                yield return (solverName);

            yield return ("=== Steps ===");
            string storage = "";
            foreach (int step in steps)
                storage += (step.ToString() + "; ");
            yield return storage;

            yield return ("=== Seeds ===");
            yield return (seeds.Count().ToString());

            yield return ("=== Total ===");
            yield return ("Experiments: " + input.Count.ToString());

            yield return ("=== Details ===");
            foreach(string s in MakeReportDetailed(input))
                yield return (s);
        }

        static public IEnumerable<string> MakeReportDetailed(ExperimentCollection input)
        {

            foreach (var experimentGroup in input.GroupBy((d) => d.Problem.AsString()).Select(gr => gr))
            {
                string groupName = experimentGroup.Key;
                double min = double.MaxValue;
                double[] minVector = null;
                //experimentGroup.Select( ex => ex.Solutions.Min( s => s.ResultValue)
                //int count5p = 0;
                //int count05p = 0;
                foreach (ExperimentDescriptor ex in experimentGroup)
                {
                    double exMin = ex.Solutions.Min(s => s.ResultValue);
                    int lastStep = ex.Solutions.Max(s => s.Step);
                    Solution lastSolution = ex.Solutions.First(s => s.Step == lastStep);

                    if (lastSolution.ResultValue < min)
                    {
                        min = lastSolution.ResultValue;
                        minVector = lastSolution.Result;
                    }
                }

                yield return ("=== " + groupName + " ===");
                yield return ("Value: " + min.ToString());
                yield return ("At: " + new Vector(minVector).ToString());


            }

            //yield return "";

        }


        /// <summary>
        /// Redefines steps in ExperimentCollection according to parameter steps
        /// </summary>
        /// <param name="input">Collection to be redefined</param>
        /// <param name="steps">New steps</param>
        /// <returns></returns>
        static public ExperimentCollection RedefineSteps(ExperimentCollection input, int[] steps)
        {
            ExperimentDescriptor[] grouping = input.GroupBy((d) => (d.Problem.AsString() + d.Solver.AsString())).Select(gr => gr.First()).ToArray();
            int[] seeds = input.GroupBy((d) => d.Seed).Select(gr => gr.Key).ToArray();

            ExperimentCollection result = new ExperimentCollection();
            foreach (ExperimentDescriptor ed in grouping)
                foreach (int seed in seeds)
                {
                    ExperimentDescriptor newEd = ed.Copy();
                    newEd.Seed = seed;
                    List<Solution> sList = new List<Solution>();

                    foreach (int step in steps)
                    {
                        Solution newSolution = new Solution();
                        newSolution.Step = step;
                        sList.Add(newSolution);
                    }
                    newEd.Solutions = sList.ToArray();
                    result.Add(newEd);
                }

            return result;
        }

        /// <summary>
        /// Converts to csv with delimiter \t (tabelator)
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        static public IEnumerable<string> ConvertToCSV(ExperimentCollection input)
        {
            foreach (ExperimentDescriptor ed in input)
                yield return (ed.ToString());
        }

        /// <summary>
        /// Redefines seeds in ExperimentCollection according to parameter seeds
        /// </summary>
        /// <param name="input">Collection to be redefined</param>
        /// <param name="seeds">New seeds</param>
        /// <returns></returns>
        static public ExperimentCollection RedefineSeeds(ExperimentCollection input, int[] seeds)
        {
            ExperimentDescriptor[] grouping = input.GroupBy((d) => (d.Problem.AsString() + d.Solver.AsString())).Select(gr => gr.First()).ToArray();

            int[] steps = grouping[0].GetSteps().ToArray();//input.GroupBy((d) => d.Steps).Select(gr => gr.Key).ToArray();
            //int[] seeds = input.GroupBy((d) => d.Seed).Select(gr => gr.Key).ToArray();

            ExperimentCollection result = new ExperimentCollection();
            foreach (ExperimentDescriptor ed in grouping)
                foreach (int seed in seeds)
                {
                    ExperimentDescriptor newEd = ed.Copy();
                    newEd.Seed = seed;
                    List<Solution> solutions = new List<Solution>();
                    foreach (int step in steps)
                    {
                        Solution newSolution = new Solution();
                        newSolution.Step = step;
                        solutions.Add(newSolution);
                    }
                    newEd.Solutions = solutions.ToArray();
                    result.Add(newEd);
                }
            return result;
        }

        public static ExperimentCollection Combine(ExperimentCollection problemsCol, ExperimentCollection solversCol)
        {
            ExperimentDescriptor[] parametrizedSolvers = solversCol.GroupBy((d) => (d.Solver.AsString())).Select(gr => gr.First()).ToArray();
            ExperimentDescriptor[] problems = problemsCol.GroupBy((d) => (d.Problem.AsString())).Select(gr => gr.First()).ToArray();
            ExperimentCollection result = new ExperimentCollection();

            foreach (ExperimentDescriptor edSolver in parametrizedSolvers)
                foreach (ExperimentDescriptor edProblem in problems)
                {
                    ExperimentDescriptor ed = edSolver.Copy();
                    ed.Problem = edProblem.Problem.Copy();
                    result.Add(ed);
                }

            return result;
        }

        /// <summary>
        /// Executes all experiments described by items in ec parameter. If (domainCount != 1) then execution is parallelized.
        /// Each parallelizm is invoked in separate domain to manage random sequences. Each domain has only one thread and then only one random sequence.
        /// </summary>
        /// <param name="ec">List of experiments</param>
        /// <param name="domainCount">Number of threads for execution</param>
        /// <returns></returns>
        public ExperimentCollection Execute(ExperimentCollection ec, int domainCount)
        {

            ExperimentCollection result = new ExperimentCollection();

            //make copy
            List<ExperimentDescriptor> ecCopy = ec.Select(ed => ed).ToList();

            if (domainCount == 1)
            {
                //execute experiments in single (current) domain

                ExperimentDescriptor[] anotherResults = ExecuteInCurrentDomain(ecCopy.ToArray());
                result.AddRange(anotherResults);
            }
            else
            {

                //create separate lists of experiments, one list for one domain
                List<ExperimentCollection> edList = new List<ExperimentCollection>();
                for (int i = 0; i < domainCount; i++)
                    edList.Add(new ExperimentCollection());

                int index = 0;
                while (ecCopy.Count() > 0)
                {
                    index++;
                    if (index >= domainCount)
                        index = 0;

                    edList[index].Add(ecCopy.First());
                    ecCopy.RemoveAt(0);
                }

                //lets run lists in separate domains
                /*
                Parallel.ForEach(edList,
                    (edListItem) =>
                    {
                        //int[] steps = cedArray.Select((ed) => (ed.Steps)).ToArray();
                        ExperimentDescriptor[] anotherResults = ExecuteInAnotherDomain("Opti", edListItem, allSteps);
                        lock (result)
                        {
                            result.AddRange(anotherResults);
                        }
                    }
                    );
                */

                //this kind of implementation allows to avoid lock usage
                ExperimentCollection[] results = new ExperimentCollection[edList.Count];
                Parallel.For(0, edList.Count,
                    (i) =>
                    {
                        //int[] steps = cedArray.Select((ed) => (ed.Steps)).ToArray();
                        ExperimentDescriptor[] anotherResults = ExecuteInAnotherDomain("Opti_" + i.ToString() + "_", edList[i]);
                        results[i] = new ExperimentCollection(anotherResults);
                        Console.WriteLine("Domain " + i.ToString() + " finished");
                    }
                );

                foreach (ExperimentCollection currentCollection in results)
                    result.AddRange(currentCollection);

            }

            return result;
        }

        /// <summary>
        /// Executes all experiments in current domain at one thread.
        /// All experiments will be logged at all steps.
        /// </summary>
        /// <param name="eds">List of experiments to execution</param>
        /// <param name="steps">List of steps for log</param>
        /// <returns></returns>
        private ExperimentDescriptor[] ExecuteInCurrentDomain(ExperimentDescriptor[] eds)
        {
            List<ExperimentDescriptor> result = new List<ExperimentDescriptor>();

            foreach (ExperimentDescriptor ed in eds)
            {
                int[] sortedSteps = ed.Solutions.Select(s => s.Step).OrderBy((c) => (c)).ToArray();
                //int[] sortedSteps = allSteps.OrderBy((c) => (c)).ToArray();
                int firstStep = sortedSteps.First();
                int lastStep = sortedSteps.Last();

                if (ed != null)
                {
                    ExperimentDescriptor currentDescriptor = ed.Copy();
                    currentDescriptor.Solutions = new Solution[0];

                    ISolver solver = Experiment.Run(ref currentDescriptor, firstStep);

                    for (int i = 1; i < sortedSteps.Count(); i++)
                    {
                        Experiment.Continue(ref currentDescriptor, ref solver, sortedSteps[i - 1], sortedSteps[i]);
                    }
                    result.Add(currentDescriptor.Copy());
                }
            }
            return result.ToArray();
        }

        /// <summary>
        /// Executes all experiments in new domain at one thread.
        /// All experiments will be logged at all steps.
        /// </summary>
        /// <param name="eds">List of experiments to execution</param>
        /// <param name="steps">List of steps for log</param>
        /// <returns></returns>
        private ExperimentDescriptor[] ExecuteInAnotherDomain(string DomainName, ExperimentCollection edCollection)
        {
            ExperimentDescriptor[] result = null;
            AppDomain workerAppDomain = AppDomain.CreateDomain(DomainName + Guid.NewGuid());
            try
            {
                /*
                Type remoteExecutorType = typeof(ExperimentDescriptorRemoteExecutor);
                ExperimentDescriptorRemoteExecutor executor = (ExperimentDescriptorRemoteExecutor)workerAppDomain.CreateInstanceAndUnwrap(remoteExecutorType.Assembly.GetName().Name, remoteExecutorType.FullName);

                string xml = edCollection.Save();

                string resultXml = executor.Execute(xml, steps);
                ExperimentCollection ecResult = ExperimentCollection.Create(resultXml);
                result = ecResult.ToArray();
                */

                Type remoteExecutorType = typeof(ExperimentDescriptorRemoteExecutor);
                ExperimentDescriptorRemoteExecutor executor = (ExperimentDescriptorRemoteExecutor)workerAppDomain.CreateInstanceAndUnwrap(remoteExecutorType.Assembly.GetName().Name, remoteExecutorType.FullName);


                result = executor.Execute(edCollection.ToArray());


            }
            finally
            {
                AppDomain.Unload(workerAppDomain);
            }

            return result;
        }

        /// <summary>
        /// Class - proxy for execution in another domain
        /// </summary>
        class ExperimentDescriptorRemoteExecutor : MarshalByRefObject
        {
            string domainName = "";
            public ExperimentDescriptorRemoteExecutor()
            {
                domainName = AppDomain.CurrentDomain.FriendlyName;
            }

            public ExperimentDescriptor[] Execute(ExperimentDescriptor[] edArray)
            {
                ExperimentDriver eDriver = new ExperimentDriver();
                ExperimentDescriptor[] result = eDriver.ExecuteInCurrentDomain(edArray);

                return result;
            }

            public string ExecuteByStr(string edsXML)
            {
                ExperimentCollection edCollection = ExperimentCollection.Create(edsXML);

                ExperimentDriver eDriver = new ExperimentDriver();
                ExperimentDescriptor[] result = eDriver.ExecuteInCurrentDomain(edCollection.ToArray());
                ExperimentCollection eCollection = new ExperimentCollection();
                eCollection.AddRange(result);

                return eCollection.Save();
            }
        }


    }
}
